// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:args/args.dart';
import 'package:compiler/src/compiler.dart';
import 'package:compiler/src/elements/entities.dart';
import 'package:compiler/src/ir/annotations.dart';
import 'package:compiler/src/js_backend/native_data.dart';
import 'package:compiler/src/kernel/kernel_strategy.dart';
import 'package:compiler/src/kernel/element_map.dart';
import 'package:expect/async_helper.dart';
import 'package:expect/expect.dart';
import 'package:front_end/src/api_prototype/lowering_predicates.dart';
import 'package:kernel/ast.dart' as ir;

import '../helpers/args_helper.dart';
import 'package:compiler/src/util/memory_compiler.dart';

const String pathPrefix = 'sdk/tests/web/native/';

const Map<String, String> source = {
  '$pathPrefix/main.dart': '''

library lib;

import 'package:meta/dart2js.dart';

import 'jslib1.dart';
import 'jslib2.dart';
import 'nativelib.dart';

@pragma('dart2js:noInline')
method1() {}

@noInline
method2() {}

@pragma(const String.fromEnvironment('foo', defaultValue: 'dart2js:tryInline'))
method3() {}

@tryInline
method4() {}

main() {
  method1();
  method2();
  method3();
  method4();
  JsClass1()..jsMethod1()..jsMethod2();
  JsClass2();
  jsMethod3();
  NativeClass1()..nativeMethod()..nativeField;
  NativeClass2()..nativeField;
  NativeClass3()..nativeMethod()..nativeGetter;
  nativeMethod();
}
''',
  '$pathPrefix/jslib1.dart': '''

@JS('lib1')
library lib1;

import 'package:js/js.dart';

@JS('JsInteropClass1')
class JsClass1 {
  @JS('jsInteropMethod1')
  external jsMethod1();

  external jsMethod2();
}

''',
  '$pathPrefix/jslib2.dart': '''

@JS()
library lib2;

import 'package:js/js.dart';

@JS()
@anonymous
class JsClass2 {
}

@JS('jsInteropMethod3')
external jsMethod3();

external jsMethod4();
''',
  '$pathPrefix/nativelib.dart': '''
library lib3;

import 'dart:_js_helper';

@Native('Class1')
class NativeClass1 {
  @JSName('field1')
  var nativeField;

  @JSName('method1')
  nativeMethod() native;
}

@Native('Class2,!nonleaf')
class NativeClass2 {
  @JSName('field2')
  var nativeField;
}

@Native('Class3a,Class3b')
class NativeClass3 {

  @JSName('method2')
  get nativeGetter native;

  @Creates('String')
  @Returns('int')
  nativeMethod() native;
}

@JSName('method3')
nativeMethod() native;
''',
};

const Map<String, String> expectedNativeClassNames = {
  '$pathPrefix/nativelib.dart::NativeClass1': 'Class1',
  '$pathPrefix/nativelib.dart::NativeClass2': 'Class2,!nonleaf',
  '$pathPrefix/nativelib.dart::NativeClass3': 'Class3a,Class3b',
};

const Map<String, String> expectedNativeMemberNames = {
  '$pathPrefix/nativelib.dart::NativeClass1::nativeField': 'field1',
  '$pathPrefix/nativelib.dart::NativeClass1::nativeMethod': 'method1',
  '$pathPrefix/nativelib.dart::NativeClass2::nativeField': 'field2',
  '$pathPrefix/nativelib.dart::NativeClass3::nativeGetter': 'method2',
  '$pathPrefix/nativelib.dart::nativeMethod': 'method3',
};

const Map<String, String> expectedCreates = {
  '$pathPrefix/nativelib.dart::NativeClass3::nativeMethod': 'String',
};

const Map<String, String> expectedReturns = {
  '$pathPrefix/nativelib.dart::NativeClass3::nativeMethod': 'int',
};

const Map<String, String> expectedJsInteropLibraryNames = {
  '$pathPrefix/jslib1.dart': 'lib1',
  '$pathPrefix/jslib2.dart': '',
};

const Map<String, String> expectedJsInteropClassNames = {
  '$pathPrefix/jslib1.dart::JsClass1': 'JsInteropClass1',
  '$pathPrefix/jslib2.dart::JsClass2': '',
};

const Map<String, String> expectedJsInteropMemberNames = {
  '$pathPrefix/jslib1.dart::JsClass1::jsMethod1': 'jsInteropMethod1',
  '$pathPrefix/jslib2.dart::jsMethod3': 'jsInteropMethod3',
};

const Set<String> expectedAnonymousJsInteropClasses = {
  '$pathPrefix/jslib2.dart::JsClass2',
};

const Set<String> expectedNoInlineMethods = {
  '$pathPrefix/main.dart::method1',
  '$pathPrefix/main.dart::method2',
};

const Set<String> expectedTryInlineMethods = {
  '$pathPrefix/main.dart::method3',
  '$pathPrefix/main.dart::method4',
};

main(List<String> args) {
  ArgParser argParser = createArgParser();

  asyncTest(() async {
    ArgResults argResults = argParser.parse(args);
    Uri? librariesSpecificationUri = getLibrariesSpec(argResults);
    Uri? packageConfig = getPackages(argResults);
    List<String> options = getOptions(argResults);

    runTest({required bool useIr}) async {
      CompilationResult result = await runCompiler(
        entryPoint: Uri.parse('memory:$pathPrefix/main.dart'),
        memorySourceFiles: source,
        packageConfig: packageConfig,
        librariesSpecificationUri: librariesSpecificationUri,
        options: options,
      );
      Expect.isTrue(result.isSuccess);
      Compiler compiler = result.compiler!;
      KernelFrontendStrategy frontendStrategy = compiler.frontendStrategy;
      KernelToElementMap elementMap = frontendStrategy.elementMap;
      ir.Component component = elementMap.env.mainComponent;
      IrAnnotationData annotationData =
          frontendStrategy.irAnnotationDataForTesting;

      void testAll(NativeData nativeData) {
        void testMember(
          String idPrefix,
          ir.Member member, {
          required bool implicitJsInteropMember,
          required bool implicitNativeMember,
        }) {
          if (memberIsIgnorable(member)) return;
          String memberId = '$idPrefix::${member.name.text}';
          MemberEntity memberEntity = elementMap.getMember(member);

          String? expectedJsInteropMemberName =
              expectedJsInteropMemberNames[memberId];
          String? expectedNativeMemberName =
              expectedNativeMemberNames[memberId];
          Set<String> expectedPragmaNames = {};
          if (expectedNoInlineMethods.contains(memberId)) {
            expectedPragmaNames.add('dart2js:noInline');
          }
          if (expectedTryInlineMethods.contains(memberId)) {
            expectedPragmaNames.add('dart2js:tryInline');
          }

          String? expectedCreatesText = expectedCreates[memberId];
          String? expectedReturnsText = expectedReturns[memberId];

          if (useIr) {
            Expect.equals(
              expectedJsInteropMemberName,
              annotationData.getJsInteropMemberName(member),
              "Unexpected js interop member name from IR for $member, "
              "id: $memberId",
            );

            Expect.equals(
              expectedNativeMemberName,
              annotationData.getNativeMemberName(member),
              "Unexpected js interop member name from IR for $member, "
              "id: $memberId",
            );

            List<PragmaAnnotationData> pragmaAnnotations = annotationData
                .getMemberPragmaAnnotationData(member);
            Set<String> pragmaNames =
                pragmaAnnotations.map((d) => d.name).toSet();
            Expect.setEquals(
              expectedPragmaNames,
              pragmaNames,
              "Unexpected pragmas from IR for $member, "
              "id: $memberId",
            );

            List<String> createsAnnotations = annotationData
                .getCreatesAnnotations(member);
            Expect.equals(
              expectedCreatesText,
              createsAnnotations.isEmpty ? null : createsAnnotations.join(','),
              "Unexpected create annotations from IR for $member, "
              "id: $memberId",
            );

            List<String> returnsAnnotations = annotationData
                .getReturnsAnnotations(member);
            Expect.equals(
              expectedReturnsText,
              returnsAnnotations.isEmpty ? null : returnsAnnotations.join(','),
              "Unexpected returns annotations from IR for $member, "
              "id: $memberId",
            );
          }

          bool isJsInteropMember =
              (implicitJsInteropMember && member.isExternal) ||
              expectedJsInteropMemberName != null;
          Expect.equals(
            isJsInteropMember,
            nativeData.isJsInteropMember(memberEntity),
            "Unexpected js interop member result from native data for $member, "
            "id: $memberId",
          );
          Expect.equals(
            isJsInteropMember
                ? expectedJsInteropMemberName ?? memberEntity.name
                : null,
            nativeData.getJsInteropMemberName(memberEntity),
            "Unexpected js interop member name from native data for $member, "
            "id: $memberId",
          );

          bool isNativeMember =
              implicitNativeMember || expectedNativeMemberName != null;
          Expect.equals(
            isNativeMember || isJsInteropMember,
            nativeData.isNativeMember(memberEntity),
            "Unexpected native member result from native data for $member, "
            "id: $memberId",
          );
          Expect.equals(
            isNativeMember
                ? expectedNativeMemberName ?? memberEntity.name
                : (isJsInteropMember
                    ? expectedJsInteropMemberName ?? memberEntity.name
                    : null),
            nativeData.getFixedBackendName(memberEntity),
            "Unexpected fixed backend name from native data for $member, "
            "id: $memberId",
          );

          if (expectedCreatesText != null) {
            String createsText;
            if (memberEntity is FieldEntity) {
              createsText = nativeData
                  .getNativeFieldLoadBehavior(memberEntity)
                  .typesInstantiated
                  .join(',');
            } else {
              createsText = nativeData
                  .getNativeMethodBehavior(memberEntity as FunctionEntity)
                  .typesInstantiated
                  .join(',');
            }
            Expect.equals(
              expectedCreatesText,
              createsText,
              "Unexpected create annotations from native data for $member, "
              "id: $memberId",
            );
          }

          if (expectedReturnsText != null) {
            String returnsText;
            if (memberEntity is FieldEntity) {
              returnsText = nativeData
                  .getNativeFieldLoadBehavior(memberEntity)
                  .typesReturned
                  .join(',');
            } else {
              returnsText = nativeData
                  .getNativeMethodBehavior(memberEntity as FunctionEntity)
                  .typesReturned
                  .join(',');
            }
            Expect.equals(
              expectedReturnsText,
              returnsText,
              "Unexpected returns annotations from native data for $member, "
              "id: $memberId",
            );
          }

          List<PragmaAnnotationData> pragmaAnnotations = frontendStrategy
              .modularStrategyForTesting
              .getPragmaAnnotationData(member);
          Set<String> pragmaNames =
              pragmaAnnotations.map((d) => d.name).toSet();
          Expect.setEquals(
            expectedPragmaNames,
            pragmaNames,
            "Unexpected pragmas from modular strategy for $member, "
            "id: $memberId",
          );
        }

        for (ir.Library library in component.libraries) {
          if (library.importUri.isScheme('memory')) {
            String libraryId = library.importUri.path;
            LibraryEntity libraryEntity = elementMap.getLibrary(library);

            String? expectedJsInteropLibraryName =
                expectedJsInteropLibraryNames[libraryId];
            if (useIr) {
              Expect.equals(
                expectedJsInteropLibraryName,
                annotationData.getJsInteropLibraryName(library),
                "Unexpected js library name from IR for $library",
              );
            }
            Expect.equals(
              expectedJsInteropLibraryName != null,
              nativeData.isJsInteropLibrary(libraryEntity),
              "Unexpected js library result from native data for $library",
            );
            Expect.equals(
              expectedJsInteropLibraryName,
              nativeData.getJsInteropLibraryName(libraryEntity),
              "Unexpected js library name from native data for $library",
            );

            for (ir.Class cls in library.classes) {
              String clsId = '$libraryId::${cls.name}';
              ClassEntity classEntity = elementMap.getClass(cls);

              String? expectedNativeClassName = expectedNativeClassNames[clsId];
              if (useIr) {
                Expect.equals(
                  expectedNativeClassName,
                  annotationData.getNativeClassName(cls),
                  "Unexpected native class name from IR for $cls",
                );
              }
              bool isNativeClass =
                  nativeData.isNativeClass(classEntity) &&
                  !nativeData.isJsInteropClass(classEntity);
              String? nativeDataClassName;
              if (isNativeClass) {
                nativeDataClassName = nativeData
                    .getNativeTagsOfClass(classEntity)
                    .join(',');
                if (nativeData.hasNativeTagsForcedNonLeaf(classEntity)) {
                  nativeDataClassName += ',!nonleaf';
                }
              }
              Expect.equals(
                expectedNativeClassName != null,
                isNativeClass,
                "Unexpected native class result from native data for $cls",
              );

              Expect.equals(
                expectedNativeClassName,
                nativeDataClassName,
                "Unexpected native class name from native data for $cls",
              );

              String? expectedJsInteropClassName =
                  expectedJsInteropClassNames[clsId];
              if (useIr) {
                Expect.equals(
                  expectedJsInteropClassName,
                  annotationData.getJsInteropClassName(cls),
                  "Unexpected js class name from IR for $cls",
                );
              }
              Expect.equals(
                expectedJsInteropClassName != null,
                nativeData.isJsInteropClass(classEntity),
                "Unexpected js class result from native data for $cls",
              );
              Expect.equals(
                expectedJsInteropClassName,
                nativeData.getJsInteropClassName(classEntity),
                "Unexpected js class name from native data for $cls",
              );

              bool expectedAnonymousJsInteropClass =
                  expectedAnonymousJsInteropClasses.contains(clsId);
              if (useIr) {
                Expect.equals(
                  expectedAnonymousJsInteropClass,
                  annotationData.isAnonymousJsInteropClass(cls),
                  "Unexpected js anonymous class result from IR for $cls",
                );
              }
              Expect.equals(
                expectedAnonymousJsInteropClass,
                nativeData.isAnonymousJsInteropClass(classEntity),
                "Unexpected js anonymous class result from native data for "
                "$cls",
              );

              for (ir.Member member in cls.members) {
                testMember(
                  clsId,
                  member,
                  implicitJsInteropMember: nativeData.isJsInteropClass(
                    classEntity,
                  ),
                  implicitNativeMember:
                      member is! ir.Constructor &&
                      !isTearOffLowering(member) &&
                      nativeData.isNativeClass(classEntity) &&
                      !nativeData.isJsInteropClass(classEntity),
                );
              }
            }
            for (ir.Member member in library.members) {
              testMember(
                libraryId,
                member,
                implicitJsInteropMember: expectedJsInteropLibraryName != null,
                implicitNativeMember: false,
              );
            }
          }
        }
      }

      testAll(compiler.frontendClosedWorldForTesting!.nativeData);
      if (useIr) {
        // We need to open the environment because creating annotation data
        // from IR will create K-model classes and members for all annotations
        // in the IR component, and not just the ones queried specifically for
        // JS-interop and pragma-like annotations.
        elementMap.envIsClosed = false;
        testAll(NativeData.fromIr(elementMap, annotationData));
      }
    }

    print('test annotations from K-model');
    await runTest(useIr: false);

    print('test annotations from IR');
    await runTest(useIr: true);
  });
}
